# 40-Pin Expansion Header ***Copyright © Quectel Wireless Solutions Co., Ltd. 2026. All rights reserved.*** --- **Quectel Pi** smart controller board provides a standard 40-pin GPIO expansion header that supports various peripheral interfaces, including GPIO, I2C, SPI, UART, and PWM. The following sections describe how to test these interfaces. ```{image} images/image_TRxPb48LaodiXrxWfhecWPx0nuc.webp :width: 1371px :height: 802px :align: center ``` # Pin Definition

Function10

Function9

Function8

Function7

Function6

Function5

Function4

Function3

M2 GPIO#

Pin#

Pin#

M2 GPIO#

Function3

Function4

Function5

Function6

Function7

Function8

Function9

Function10

VCC 3V3

1

2

VCC 5V










CAN1_RX_M2

I2C3_SDA_M0

UART2_RX_M1

-




GPIO4_B4_d(I2C3_SDA)

3

4

VCC 5V







CAN1_TX_M2

PCIE0_CLKREQN_M2

I2C3_SCL_M0

UART2_TX_M1

-




GPIO4_B5_d(I2C3_SCL)

5

6

GND







I2C8_SDA_M2

UART8_CTSN_M1

UART7_RX_M0

SAI0_LRCK_M0

ETH0_RXD2_M1




GPIO2_B7_d

7

8

GPIO2_B0_d(UART1_TXD)




SDMMC1_D2_M1

ETH0_TXD1_M1

SAI0_SDI0_M0

PDM0_SDI3_M3

UART1_TX_M1







GND

9

10

GPIO2_B1_d(UART1_RXD)




SDMMC1_D3_M1

ETH0_TXD0_M1

SAI0_SDI1_M0

PDM0_SDI2_M3

UART1_RX_M1










I2C8_SCL_M2

UART8_RTSN_M1

UART7_TX_M0

SAI0_SCLK_M0




-




GPIO2_B6_d

11

12

GPIO2_C7_d




ETH1_TXD1_M0

SAI4_LRCK_M3

UART4_RTSN_M0

I2C5_SDA_M2

PWM0_CH1_M2










SPI4_CSN1_M3

I2C4_SCL_M2

UART8_TX_M1

SAI0_SDO0_M0




SDMMC1_D0_M1




GPIO2_A6_d

13

14

GND




-

I2C4_SDA_M2

UART8_RX_M1

SAI0_SDO1_M0




SDMMC1_D1_M1




GPIO2_A7_d

15

16

GPIO4_A4_d

SAI4_SCLK_M0

PDM1_SDI3_M1

-

SPI3_MOSI_M2

UART6_TX_M0

I2C4_SCL_M1

CAN0_TX_M2




VCC 3V3

17

18

GPIO4_A6_d

SAI4_LRCK_M0

PDM1_CLK0_M1

-

SPI3_MISO_M2

UART6_RX_M0

I2C4_SDA_M1

CAN0_RX_M2




PWM1_CH2_M2

SPI1_MOSI_M1

UART11_CTSN_M1

PDM1_SDI2_M0

SAI2_SCLK_M1

-







GPIO2_C2_d(SPI_MOSI)

19

20

GND




PWM0_CH0_M2

SPI1_MISO_M1

UART11_RTSN_M1

PDM1_SDI3_M0

SAI2_LRCK_M1







GPIO2_C3_d(SPI_MISO)

21

22

GPIO2_B4_d




SDMMC1_PWREN_M1

ETH0_TXD2_M1

SAI0_SDI3_M0




UART7_CTSN_M0

SPI4_MOSI_M3

SATA0_ACTLED_M0




PWM1_CH4_M2

SPI1_CLK_M1

UART11_RX_M1

PDM1_CLK0_M0

SAI2_SDI_M1







GPIO2_C5_d(SPI_CLK)

23

24

GPIO2_C4_d(SPI_CE0)







SAI2_SDO_M1

PDM1_SDI0_M0

UART11_TX_M1

SPI1_CSN0_M1

PWM1_CH3_M2

-

GND

25

26

GPIO2_C1_d(SPI_CE1)







-

SAI2_MCLK_M1




UART9_TX_M0

SPI1_CSN1_M1

PWM1_CH1_M2




I2C7_SDA_M1

SPI3_MOSI_M0

UART3_RX_M0

SAI3_LRCK_M2

ETH0_MDC_M1







GPIO3_A1_d(I2C7_SDA)

27

28

GPIO3_A0_d(I2C7_SCL)







-

SAI3_SCLK_M2

UART3_TX_M0

SPI3_CLK_M0

I2C7_SCL_M1

-






 

PWM2_CH5_M2

I2C9_SCL_M2

UART6_CTSN_M1







GPIO2_D5_d

29

30

GND






 

PWM2_CH4_M2

I2C9_SDA_M2

UART6_RTSN_M1







GPIO2_D4_d

31

32

GPIO2_B5_d




SDMMC1_DETN_M1

ETH0_RXCLK_M1

SAI0_MCLK_M0

PDM0_CLK0_M3

UART7_RTSN_M0

SPI4_MISO_M3

SATA1_ACTLED_M0






 

PWM2_CH3_M2

I3C1_SDA_M0

UART6_RX_M1







GPIO2_D3_d(PWM)

33

34

GND







PWM1_CH5_M2

I2C5_SCL_M2

UART4_CTSN_M0

SAI4_SCLK_M3







GPIO2_C6_d

35

36

GPIO2_D2_d

CAM_CLK0_OUT_M1




SAI4_MCLK_M3

UART6_TX_M1

I3C1_SCL_M0

PWM2_CH2_M2







PWM2_CH7_M2

SPI3_CSN1_M0

UART9_CTSN_M0

-

SAI0_SDO3_M0

ETH_CLK0_25M_OUT_M1




CAM_CLK2_OUT_M1

GPIO2_D7_d

37

38

GPIO2_D0_d

-
 

SAI4_SDI_M3

UART4_TX_M0

I2C6_SCL_M2

PWM2_CH0_M2







GND

39

40

GPIO2_D1_d

ETH1_RXD0_M0

SAI4_SDO_M3

UART4_RX_M0

I2C6_SDA_M2

PWM2_CH1_M2

-






# GPIO Test ## Hardware Connection 1. Install the **GPIO terminal expansion board** directly onto the 40Pin header. When a signal is driven high or low, the LED corresponding to that Pin turns on or off. ```{image} images/image_C2QFb8QbhoKdpFxVIOZc2pELn4d.webp :width: 1280px :height: 960px ``` Note: There are currently 16 general-purpose GPIOs. The pins corresponding to the following multiplexed functions are occupied. You can use [qpi-config](<../qpi-config/qpi-config.md>) to disable the corresponding multiplexed functions and use the pins as general-purpose GPIOs. - Pin3/5:I2C3 - Pin8/10:UART1 - Pin19/21/23/24/26:SPI1 - Pin27/28:I2C7 - Pin33:PWM 1. Use a **multimeter to measure** the voltage at the Pin corresponding to the GPIO. The high level should be 3.3V, and the low level should be 0V. ## Test Method Using Pin13(GPIO2_A6_d) as an example, enter adb shell and use the gpiod commands to test the GPIO. ```bash adb shell # List GPIO controllers gpiodetect # View Pin13 GPIO2_A6 information gpioinfo -c gpiochip2 6 # Read the current level gpioget -c gpiochip2 6 # Drive high for 10 seconds gpioset -c gpiochip2 -t 10s,0 6=1 # Drive low for 10 seconds gpioset -c gpiochip2 -t 10s,0 6=0 ``` **GPIO Interrupt Test Method:** Connect the GPIO under test to a button or an external signal source so that the pin level changes. Use gpiomon to monitor rising-edge and falling-edge events on the GPIO. GPIO2_A6_d is again used as the example here. ```bash # Read the current level of GPIO2_A6 gpioget -c gpiochip2 6 # Monitor rising-edge and falling-edge events on GPIO2_A6 and exit after 5 events gpiomon -n 5 -e rising -c gpiochip2 6 gpiomon -n 5 -e falling -c gpiochip2 6 ``` # I2C Test ## Environment Sensor HAT Test: pin3 and pin5 of the 40-pin header are configured as the I2C data and clock pins by default. To test the I2C interface, an external I2C device is required. This test uses the [Waveshare Environment Sensor HAT](), whose corresponding device node is `/dev/i2c-` `3`。 This test uses the Waveshare Environment Sensor HAT, connected through the 40-pin header. **Hardware Connection Diagram:** ```{image} images/image_M0U8bVhsqouXilxKWjRcytPmnJc.webp :width: 1280px :height: 960px ``` Quectel Pi M2 with the expansion board installed ## I2C Interface Verification (Waveshare Environment Sensor HAT) ### Bus and Device Detection ```bash adb shell ls /dev/i2c* # Confirm that /dev/i2c-3 exists i2cdetect -y 3 # 0x29 0x53 0x68 0x76 should be listed (this board also lists 0x28) ``` ### Read the ID Register of Each IC ```python # TSL2591: ID reg 0x12 requires command bit 0xA0; therefore, 0xA0|0x12=0xB2 i2cget -y 3 0x29 0xB2 b # Expected: 0x50 # BME280:ID reg 0xD0 i2cget -y 3 0x76 0xD0 b # Expected: 0x60 # LTR390:ID reg 0x06 i2cget -y 3 0x53 0x06 b # Expected: 0xB2 # IMU: Select bank 0 first, then read WHO_AM_I (reg 0x00) i2cset -y 3 0x68 0x7F 0x00 b i2cget -y 3 0x68 0x00 b # Expected: 0xEA ``` **Note: When the sensor board is connected directly to the 40PIN header, Pin27 and Pin28 are not supported. The board's 40PIN header connects only to the IIC signals on Pin3 and Pin5, not Pin27 and Pin28. Pin27 and Pin28 can be connected using jumper wires.** # SPI Test On the **40-pin** header, Pin19/21/23/24/26 are configured for the **SPI** function by default. The corresponding device node is **SPI1** 。 **View Serial Devices** Use the following command to view all serial devices in the system: ```bash ls /dev/spidev* /dev/spidev1.0 /dev/spidev1.1 ``` ## SPI Loopback Test **Hardware connection: Pin19 MOSI** and **Pin21 MISO** must be shorted together. ```plaintext #!/bin/bash # SPI loopback test (extracted from 40Pin-ctrl_M2.sh) # # Usage: # ./spi_test.sh # # Wiring: connect Pin 19 (SPI1 MOSI) to Pin 21 (SPI1 MISO). # Tests /dev/spidev1.0 and /dev/spidev1.1 at 100 kHz, 8 bits/word, mode 0. # NOTE: MOSI/MISO loopback verifies SPI transfer and CLK. # Verify Pin 24/26 CS waveforms with an oscilloscope or logic analyzer. die() { echo "ERROR: $*" >&2 exit 1 } require_root() { [ "$(id -u)" -eq 0 ] || die "must be run as root" } require_command() { command -v "$1" >/dev/null 2>&1 || die "$1 is not installed" } spi_test() { require_command python3 [ -c /dev/spidev1.0 ] || die "/dev/spidev1.0 does not exist" [ -c /dev/spidev1.1 ] || die "/dev/spidev1.1 does not exist" echo "Connect Pin 19 (SPI1 MOSI) to Pin 21 (SPI1 MISO)." python3 - /dev/spidev1.0 /dev/spidev1.1 <<'PY' import ctypes import fcntl import os import struct import sys SPI_IOC_WR_MODE = 0x40016B01 SPI_IOC_WR_BITS_PER_WORD = 0x40016B03 SPI_IOC_WR_MAX_SPEED_HZ = 0x40046B04 SPI_IOC_MESSAGE_1 = 0x40206B00 PAYLOAD = bytes((0x55, 0xAA, 0x00, 0xFF, 0x12, 0x34, 0x56, 0x78)) def test_device(device): tx = (ctypes.c_ubyte * len(PAYLOAD))(*PAYLOAD) rx = (ctypes.c_ubyte * len(PAYLOAD))() transfer = bytearray(struct.pack( "=QQIIHBBBBBB", ctypes.addressof(tx), ctypes.addressof(rx), len(PAYLOAD), 100000, 0, 8, 0, 0, 0, 0, 0, )) fd = os.open(device, os.O_RDWR) try: fcntl.ioctl(fd, SPI_IOC_WR_MODE, struct.pack("=B", 0)) fcntl.ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, struct.pack("=B", 8)) fcntl.ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, struct.pack("=I", 100000)) fcntl.ioctl(fd, SPI_IOC_MESSAGE_1, transfer) finally: os.close(fd) received = bytes(rx) print("%s TX: %s" % (device, PAYLOAD.hex(" ").upper())) print("%s RX: %s" % (device, received.hex(" ").upper())) if received != PAYLOAD: raise RuntimeError("%s loopback data mismatch" % device) print("PASS: %s loopback data matched" % device) try: for path in sys.argv[1:]: test_device(path) except (OSError, RuntimeError) as exc: print("FAIL: %s" % exc, file=sys.stderr) sys.exit(1) PY status=$? [ "$status" -eq 0 ] || return "$status" echo "NOTE: MOSI/MISO loopback verifies SPI transfer and CLK." echo " Verify Pin 24/26 CS waveforms with an oscilloscope or logic analyzer." } require_root spi_test ``` The script tests `/dev/spidev1.0` and `/dev/spidev1.1`, prints the TX/RX data, and compares the results. **Test Result:** ```plaintext Connect Pin 19 (SPI1 MOSI) to Pin 21 (SPI1 MISO). /dev/spidev1.0 TX: 55 AA 00 FF 12 34 56 78 /dev/spidev1.0 RX: 55 AA 00 FF 12 34 56 78 PASS: /dev/spidev1.0 loopback data matched /dev/spidev1.1 TX: 55 AA 00 FF 12 34 56 78 /dev/spidev1.1 RX: 55 AA 00 FF 12 34 56 78 PASS: /dev/spidev1.1 loopback data matched NOTE: MOSI/MISO loopback verifies SPI transfer and CLK. Verify Pin 24/26 CS waveforms with an oscilloscope or logic analyzer. ``` # UART Test On the **40-pin** header, **pin8** and **pin10** are configured for the **UART** function by default. The corresponding device node is **/dev/ttyS** **1**。 **View Serial Devices** Use the following command to view all serial devices in the system: ```bash ls /dev/tty* ``` ## UART Loopback Test This test verifies serial-port transmit and receive operation by shorting **pin8** and **pin10** together. **Hardware connection:** Short pin8(TX) and pin10(RX) on the 40-pin header together. Create the `uart_loopback_test.sh` file with the following content: ```cpp #!/bin/bash # UART loopback test (extracted from 40Pin-ctrl_M2.sh) # # Usage: # ./uart_loopback_test.sh [-n 1] [-b 115200] # -n UART number, maps to /dev/ttyS (default 1) # -b baud rate (default 115200) # # Wiring: connect Pin 8 (UART1 TX) to Pin 10 (UART1 RX). die() { echo "ERROR: $*" >&2 exit 1 } require_root() { [ "$(id -u)" -eq 0 ] || die "must be run as root" } require_command() { command -v "$1" >/dev/null 2>&1 || die "$1 is not installed" } uart_test() { device=/dev/ttyS1 baud=115200 while [ "$#" -gt 0 ]; do case "$1" in -n) [ "$#" -ge 2 ] || die "missing UART number after -n" case "$2" in ''|*[!0-9]*) die "invalid UART number: $2" ;; esac device="/dev/ttyS$2" shift 2 ;; -b) [ "$#" -ge 2 ] || die "missing baud rate after -b" case "$2" in ''|*[!0-9]*) die "invalid baud rate: $2" ;; esac baud=$2 shift 2 ;; *) die "usage: $0 [-n 1] [-b 115200]" ;; esac done [ -c "$device" ] || die "$device does not exist" require_command stty require_command timeout require_command cmp echo "Connect Pin 8 (UART1 TX) to Pin 10 (UART1 RX)." echo "Testing $device at $baud baud..." tmp_dir=$(mktemp -d /tmp/uart-loopback.XXXXXX) || die "cannot create temporary directory" trap 'rm -rf "$tmp_dir"' EXIT INT TERM printf 'UART1_LOOPBACK\n' > "$tmp_dir/expected" stty -F "$device" "$baud" raw -echo cs8 -cstopb -parenb \ -crtscts -ixon -ixoff || die "failed to configure $device" timeout 5 dd if="$device" of="$tmp_dir/received" bs=1 count=15 status=none & rx_pid=$! sleep 1 printf 'UART1_LOOPBACK\n' > "$device" wait "$rx_pid" rx_status=$? if [ "$rx_status" -ne 0 ]; then die "UART loopback timed out or received incomplete data" fi if ! cmp -s "$tmp_dir/expected" "$tmp_dir/received"; then echo "Received bytes:" od -An -tx1 "$tmp_dir/received" die "UART loopback data mismatch" fi echo "PASS: UART loopback sent and received 15 identical bytes" rm -rf "$tmp_dir" trap - EXIT INT TERM } require_root uart_test "$@" ``` **Usage (on the board):** ```bash # Make the script executable chmod +x uart_loopback_test.sh # Default: /dev/ttyS1, 115200; wiring: Pin8(TX)<->Pin10(RX) ./uart_loopback_test.sh # Specify the UART number and baud rate ./uart_loopback_test.sh -n 1 -b 9600 ``` **Test Result:** ```plaintext Connect Pin 8 (UART1 TX) to Pin 10 (UART1 RX). Testing /dev/ttyS1 at 115200 baud... PASS: UART loopback sent and received 15 identical bytes ``` # PWM Test Wiring: Pin33 (PWM, GPIO2_D3) ↔ Pin31 (GPIO92, GPIO2_D4) (default) Create the `pwm_test.sh` file with a duty cycle of 50% and a frequency of 1000Hz, using the following content: ```bash #!/bin/bash # PWM output + GPIO edge capture test (extracted from 40Pin-ctrl_M2.sh) # # Usage: # ./pwm_test.sh [capture-gpio] # capture-gpio global GPIO number to capture PWM edges on # (default 92 = Pin 31; 93 = Pin 29) # # Wiring: connect Pin 33 (PWM2_CH3, GPIO2_D3) to the capture GPIO. # default Pin 31 / GPIO92 / GPIO2_D4 ; 93 = Pin 29 / GPIO2_D5. # Output: 1000 Hz, 50% duty cycle. # PASS criteria: measured 900-1100 Hz and 40-60% duty cycle, >=3 complete periods. GPIO_SYSFS=/sys/class/gpio die() { echo "ERROR: $*" >&2 exit 1 } require_root() { [ "$(id -u)" -eq 0 ] || die "must be run as root" } require_command() { command -v "$1" >/dev/null 2>&1 || die "$1 is not installed" } pwm_test() { pwm_chip= capture_gpio=${1:-92} case "$capture_gpio" in ''|*[!0-9]*) die "invalid capture GPIO: $capture_gpio" ;; esac capture_chip=gpiochip$((capture_gpio / 32)) capture_line=$((capture_gpio % 32)) tmp_file=$(mktemp /tmp/pwm-test.XXXXXX) || die "cannot create temporary file" require_command gpiomon require_command timeout require_command awk for chip in /sys/class/pwm/pwmchip*; do case "$(readlink -f "$chip/device" 2>/dev/null)" in */2ade3000.pwm) pwm_chip=$chip; break ;; esac done [ -n "$pwm_chip" ] || die "PWM2_CH3 controller 2ade3000.pwm was not found" pwm=$pwm_chip/pwm0 if [ -d "$GPIO_SYSFS/gpio$capture_gpio" ]; then echo "$capture_gpio" > "$GPIO_SYSFS/unexport" 2>/dev/null || \ die "GPIO$capture_gpio is busy" fi if [ ! -d "$pwm" ]; then echo 0 > "$pwm_chip/export" 2>/dev/null || die "cannot export pwm0" fi pwm_cleanup() { if [ -e "$pwm/enable" ] && [ "$(cat "$pwm/enable" 2>/dev/null)" = 1 ]; then echo 0 > "$pwm/enable" 2>/dev/null || true fi rm -f "$tmp_file" } pwm_exit() { exit_status=$? trap - EXIT INT TERM pwm_cleanup exit "$exit_status" } trap pwm_exit EXIT trap 'exit 130' INT trap 'exit 143' TERM if [ "$(cat "$pwm/enable" 2>/dev/null)" = 1 ]; then echo 0 > "$pwm/enable" 2>/dev/null || die "cannot disable pwm0" fi if [ "$(cat "$pwm/period" 2>/dev/null)" != 0 ]; then echo 0 > "$pwm/duty_cycle" 2>/dev/null || die "cannot reset PWM duty cycle" fi echo 1000000 > "$pwm/period" 2>/dev/null || die "cannot set PWM period" echo 500000 > "$pwm/duty_cycle" 2>/dev/null || die "cannot set PWM duty cycle" if [ "$capture_gpio" -eq 92 ]; then echo "Connect Pin 33 (PWM, GPIO2_D3) to Pin 31 (GPIO92, GPIO2_D4)." elif [ "$capture_gpio" -eq 93 ]; then echo "Connect Pin 33 (PWM, GPIO2_D3) to Pin 29 (GPIO93, GPIO2_D5)." else echo "Connect Pin 33 (PWM, GPIO2_D3) to GPIO$capture_gpio." fi echo "Output: 1000 Hz, 50% duty cycle" timeout 5 gpiomon -c "$capture_chip" -n 21 \ "$capture_line" > "$tmp_file" 2>&1 & monitor_pid=$! sleep 1 echo 1 > "$pwm/enable" 2>/dev/null || die "cannot enable pwm0" wait "$monitor_pid" monitor_status=$? echo 0 > "$pwm/enable" 2>/dev/null || true [ "$monitor_status" -eq 0 ] || die "no valid PWM edges captured on GPIO$capture_gpio" awk -v capture_gpio="$capture_gpio" ' $2 == "rising" { if (have_rise && have_fall) { period = $1 - prev_rise high = fall_time - rise_time if (period > 0 && high >= 0) { period_sum += period high_sum += high samples++ } } prev_rise = $1 rise_time = $1 have_rise = 1 have_fall = 0 } $2 == "falling" && have_rise { fall_time = $1 have_fall = 1 } END { if (samples < 3) { print "FAIL: insufficient complete PWM periods" > "/dev/stderr" exit 1 } frequency = samples / period_sum duty = 100 * high_sum / period_sum printf "Measured: %.2f Hz, %.2f%% duty cycle (%d periods)\n", frequency, duty, samples if (frequency < 900 || frequency > 1100 || duty < 40 || duty > 60) { print "FAIL: PWM measurement is outside the expected range" > "/dev/stderr" exit 1 } printf "PASS: PWM output and GPIO%d capture are working\n", capture_gpio } ' "$tmp_file" || exit 1 pwm_cleanup trap - EXIT INT TERM } require_root pwm_test "${1:-92}" ``` **Usage (on the board):** ```bash # Make the script executable chmod +x pwm_test.sh pwm_test.sh # Capture GPIO92 = Pin31 pwm_test.sh 93 # Capture GPIO93 = Pin29 (reconnect Pin33↔Pin29) pwm_test.sh 86 # Any global GPIO number (reconnect the corresponding pin) ``` **Test Result:** ```plaintext Connect Pin 33 (PWM, GPIO2_D3) to Pin 31 (GPIO92, GPIO2_D4). Output: 1000 Hz, 50% duty cycle Measured: 1000.35 Hz, 49.98% duty cycle (10 periods) PASS: PWM output and GPIO92 capture are working ```